diff options
Diffstat (limited to 'app/api/data-room/[projectId]/[fileId]/download/route.ts')
| -rw-r--r-- | app/api/data-room/[projectId]/[fileId]/download/route.ts | 246 |
1 files changed, 246 insertions, 0 deletions
diff --git a/app/api/data-room/[projectId]/[fileId]/download/route.ts b/app/api/data-room/[projectId]/[fileId]/download/route.ts new file mode 100644 index 00000000..3a3a8fdd --- /dev/null +++ b/app/api/data-room/[projectId]/[fileId]/download/route.ts @@ -0,0 +1,246 @@ +// app/api/data-room/[projectId]/[fileId]/download/route.ts +import { NextRequest, NextResponse } from 'next/server'; +import { getServerSession } from 'next-auth/next'; +import { authOptions } from '@/app/api/auth/[...nextauth]/route'; +import { FileService, type FileAccessContext } from '@/lib/services/fileService'; +import { promises as fs } from 'fs'; +import path from 'path'; +import db from "@/db/db"; +import { fileItems } from "@/db/schema/fileSystem"; +import { eq } from "drizzle-orm"; + +export async function GET( + request: NextRequest, + { params }: { params: { projectId: string; fileId: string } } +) { + try { + const session = await getServerSession(authOptions); + if (!session?.user) { + return NextResponse.json({ error: '인증이 필요합니다' }, { status: 401 }); + } + + const context: FileAccessContext = { + userId: Number(session.user.id), + userDomain: session.user.domain || 'partners', + userEmail: session.user.email, + ipAddress: request.ip || request.headers.get('x-forwarded-for') || undefined, + userAgent: request.headers.get('user-agent') || undefined, + }; + + const fileService = new FileService(); + + // 파일 접근 권한 확인 + const hasAccess = await fileService.checkFileAccess( + params.fileId, + context, + 'download' + ); + + if (!hasAccess) { + return NextResponse.json( + { error: '파일 다운로드 권한이 없습니다' }, + { status: 403 } + ); + } + + // FileService를 통해 파일 정보 가져오기 (다운로드 카운트 증가 및 로그 기록) + const file = await fileService.downloadFile(params.fileId, context); + + if (!file) { + return NextResponse.json( + { error: '파일을 찾을 수 없습니다' }, + { status: 404 } + ); + } + + // 파일 경로 확인 + if (!file.filePath) { + return NextResponse.json( + { error: '파일 경로가 없습니다' }, + { status: 404 } + ); + } + + // 실제 파일 경로 구성 + const nasPath = process.env.NAS_PATH || "/evcp_nas"; + const isProduction = process.env.NODE_ENV === "production"; + + let absolutePath: string; + if (isProduction) { + // 프로덕션: NAS 경로 사용 + const relativePath = file.filePath.replace('/api/files/', ''); + absolutePath = path.join(nasPath, relativePath); + } else { + // 개발: public 폴더 사용 + absolutePath = path.join(process.cwd(), 'public', file.filePath); + } + + // 파일 존재 여부 확인 + try { + await fs.access(absolutePath); + } catch (error) { + console.error('파일을 찾을 수 없습니다:', absolutePath); + return NextResponse.json( + { error: '파일을 찾을 수 없습니다' }, + { status: 404 } + ); + } + + // 파일 읽기 + const fileBuffer = await fs.readFile(absolutePath); + + // MIME 타입 결정 + const mimeType = getMimeType(file.name, file.mimeType); + + // 파일명 인코딩 (한글 등 특수문자 처리) + const encodedFileName = encodeURIComponent(file.name); + + // Response Headers 설정 + const headers = new Headers(); + headers.set('Content-Type', mimeType); + headers.set('Content-Length', fileBuffer.length.toString()); + headers.set('Content-Disposition', `attachment; filename*=UTF-8''${encodedFileName}`); + headers.set('Cache-Control', 'no-cache, no-store, must-revalidate'); + headers.set('Pragma', 'no-cache'); + headers.set('Expires', '0'); + + // 보안 헤더 추가 + headers.set('X-Content-Type-Options', 'nosniff'); + headers.set('X-Frame-Options', 'DENY'); + headers.set('X-XSS-Protection', '1; mode=block'); + + // 파일 스트림 반환 + return new NextResponse(fileBuffer, { + status: 200, + headers, + }); + + } catch (error) { + console.error('파일 다운로드 오류:', error); + + if (error instanceof Error) { + if (error.message.includes('권한')) { + return NextResponse.json( + { error: error.message }, + { status: 403 } + ); + } + } + + return NextResponse.json( + { error: '파일 다운로드에 실패했습니다' }, + { status: 500 } + ); + } +} + +// HEAD 요청 처리 (파일 정보만 확인) +export async function HEAD( + request: NextRequest, + { params }: { params: { projectId: string; fileId: string } } +) { + try { + const session = await getServerSession(authOptions); + if (!session?.user) { + return new NextResponse(null, { status: 401 }); + } + + const context: FileAccessContext = { + userId: Number(session.user.id), + userDomain: session.user.domain || 'partners', + userEmail: session.user.email, + ipAddress: request.ip || request.headers.get('x-forwarded-for') || undefined, + userAgent: request.headers.get('user-agent') || undefined, + }; + + const fileService = new FileService(); + + // 파일 접근 권한 확인 + const hasAccess = await fileService.checkFileAccess( + params.fileId, + context, + 'view' // HEAD 요청은 view 권한만 확인 + ); + + if (!hasAccess) { + return new NextResponse(null, { status: 403 }); + } + + // 파일 정보 조회 + const file = await db.query.fileItems.findFirst({ + where: eq(fileItems.id, params.fileId), + }); + + if (!file || !file.filePath) { + return new NextResponse(null, { status: 404 }); + } + + const headers = new Headers(); + headers.set('Content-Type', getMimeType(file.name, file.mimeType)); + headers.set('Content-Length', file.size?.toString() || '0'); + headers.set('Last-Modified', new Date(file.updatedAt).toUTCString()); + + return new NextResponse(null, { + status: 200, + headers, + }); + + } catch (error) { + console.error('HEAD 요청 오류:', error); + return new NextResponse(null, { status: 500 }); + } +} + +// MIME 타입 결정 헬퍼 함수 +function getMimeType(fileName: string, storedMimeType?: string | null): string { + // DB에 저장된 MIME 타입이 있으면 우선 사용 + if (storedMimeType) { + return storedMimeType; + } + + // 확장자 기반 MIME 타입 매핑 + const ext = path.extname(fileName).toLowerCase().substring(1); + const mimeTypes: Record<string, string> = { + // Documents + 'pdf': 'application/pdf', + 'doc': 'application/msword', + 'docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + 'xls': 'application/vnd.ms-excel', + 'xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + 'ppt': 'application/vnd.ms-powerpoint', + 'pptx': 'application/vnd.openxmlformats-officedocument.presentationml.presentation', + 'txt': 'text/plain', + 'csv': 'text/csv', + + // Images + 'jpg': 'image/jpeg', + 'jpeg': 'image/jpeg', + 'png': 'image/png', + 'gif': 'image/gif', + 'bmp': 'image/bmp', + 'webp': 'image/webp', + 'svg': 'image/svg+xml', + + // Archives + 'zip': 'application/zip', + 'rar': 'application/x-rar-compressed', + '7z': 'application/x-7z-compressed', + + // CAD + 'dwg': 'application/x-dwg', + 'dxf': 'application/x-dxf', + + // Video + 'mp4': 'video/mp4', + 'avi': 'video/x-msvideo', + 'mov': 'video/quicktime', + 'wmv': 'video/x-ms-wmv', + + // Audio + 'mp3': 'audio/mpeg', + 'wav': 'audio/wav', + 'ogg': 'audio/ogg', + }; + + return mimeTypes[ext] || 'application/octet-stream'; +}
\ No newline at end of file |
